import axios, { AxiosInstance, AxiosRequestConfig, CancelTokenSource } from 'axios'
import { message } from 'ant-design-vue'

const MAX_CACHE_COUNT = 50
const TIMEOUT = 10000

/**
 * axios 请求封装
 * @author zeng/704729872@qq.com
 * @date 2021/8/19 21:51
 */
class Http {
  #requestingMap = new Map()
  axiosInstance

  constructor (opts = {}) {
    this.axiosInstance = axios.create(Object.assign({
      baseURL: import.meta.env.VITE_HTTP_URL,
      timeout: TIMEOUT
    }, opts))
    this.#setRequestInterceptors()
    this.#setResponseInterceptors()
  }

  #setRequestInterceptors () {
    this.axiosInstance.interceptors.request.use(config => {
      // 首页发起的请求，均不做缓存
      if (!config.cancelToken && location.pathname !== '/') {
        config.cancelToken = this.#setCacheByAxiosConfig(config).token
      }
      // 配置 token 请求头
      return config
    }, Promise.reject)
  }

  #setResponseInterceptors () {
    this.axiosInstance.interceptors.response.use(response => {
        this.clearCacheByAxiosConfig(response.config)
        const hResult = response.data
        if (hResult.code !== 0) {
          message.error(hResult.message)
          return Promise.reject(hResult)
        }
        // 这里处理返回逻辑
        return hResult
      },
      error => {
        // 发生错误的请求，会没法拿到 key，需要定期清理缓存
        if (!axios.isCancel(error)) {
          message.error('网络开小差了')
        }
        this.clearCacheByAxiosConfig()
        return Promise.reject(error)
      })
  }

  /**
   * 根据 axios 的 config 对象，获取对应的请求字符串，把找个字符串作为 key 缓存起来
   * 组合后相同的字符串，会被认定是同一个请求
   * @param {AxiosRequestConfig} config
   * @return {string}
   */
  #getCacheKeyByAxiosConfig (config) {
    return [
      'url', 'method', 'params', 'data', 'headers', 'responseType'
    ].reduce((str, key) => `${str}${JSON.stringify(config[key])}`, '')
  }

  /**
   * 设置缓存的 key，如果 key 已存在，将会取消上次请求，并更新新值
   * @param {AxiosRequestConfig} config
   * @return {CancelTokenSource}
   */
  #setCacheByAxiosConfig (config) {
    const key = this.#getCacheKeyByAxiosConfig(config)
    const requesting = this.#requestingMap.get(key)
    if (requesting) {
      // 已经有数据了，重复请求，可以采用的策略：
      // 1.取消上次请求（目前采用）
      // 2.忽略本次请求
      requesting.cancelToken.cancel()
    }
    const cancelToken = axios.CancelToken.source()
    this.#requestingMap.set(key, {
      time: Date.now(),
      cancelToken
    })
    return cancelToken
  }

  /**
   * 清除缓存，可以考虑增加 string 类型，通过指定 url 去清除所有匹配的请求
   * @param {AxiosRequestConfig} [config]
   * @return {*}
   */
  clearCacheByAxiosConfig (config) {
    const requestingMap = this.#requestingMap
    if (config) {
      const key = this.#getCacheKeyByAxiosConfig(config)
      return requestingMap.delete(key)
    }
    if (requestingMap.size < MAX_CACHE_COUNT) {
      return
    }
    const now = Date.now()
    for (const [key, { time }] of requestingMap.entries()) {
      if (now - time < TIMEOUT) {
        continue
      }
      requestingMap.delete(key)
    }
  }

  /**
   * 强制清除所有已经存储了的请求
   */
  forceClearCache = () => {
    for (const requesting of this.#requestingMap.values()) {
      requesting.cancelToken.cancel()
    }
    this.#requestingMap.clear()
  }
}

/**
 * @type {AxiosInstance | Http}
 */
export const http = new Proxy(new Http(), {
  get (target, p, receiver) {
    if (Reflect.has(receiver, p)) {
      return Reflect.get(...arguments)
    }
    return Reflect.get(receiver.axiosInstance, p)
  }
})

export { axios }
